/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2006 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.filetransfer;

import org.jivesoftware.smack.XMPPException;

import java.io.*;
import java.util.concurrent.*;

/**
 * An incoming file transfer is created when the
 * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)}
 * method is invoked. It is a file being sent to the local user from another
 * user on the jabber network. There are two stages of the file transfer to be
 * concerned with and they can be handled in different ways depending upon the
 * method that is invoked on this class.
 * <p/>
 * The first way that a file is recieved is by calling the
 * {@link #recieveFile()} method. This method, negotiates the appropriate stream
 * method and then returns the <b><i>InputStream</b></i> to read the file data
 * from.
 * <p/>
 * The second way that a file can be recieved through this class is by invoking
 * the {@link #recieveFile(File)} method. This method returns immediatly and
 * takes as its parameter a file on the local file system where the file
 * recieved from the transfer will be put.
 * 
 * @author Alexander Wenckus
 */
public class IncomingFileTransfer extends FileTransfer {

	private FileTransferRequest recieveRequest;

	private InputStream inputStream;

	protected IncomingFileTransfer(FileTransferRequest request,
			FileTransferNegotiator transferNegotiator) {
		super(request.getRequestor(), request.getStreamID(), transferNegotiator);
		this.recieveRequest = request;
	}

	/**
	 * Negotiates the stream method to transfer the file over and then returns
	 * the negotiated stream.
	 * 
	 * @return The negotiated InputStream from which to read the data.
	 * @throws XMPPException
	 *             If there is an error in the negotiation process an exception
	 *             is thrown.
	 */
	public InputStream recieveFile() throws XMPPException {
		if (inputStream != null) {
			throw new IllegalStateException("Transfer already negotiated!");
		}

		try {
			inputStream = negotiateStream();
		} catch (XMPPException e) {
			setException(e);
			throw e;
		}

		return inputStream;
	}

	/**
	 * This method negotitates the stream and then transfer's the file over the
	 * negotiated stream. The transfered file will be saved at the provided
	 * location.
	 * <p/>
	 * This method will return immedialtly, file transfer progress can be
	 * monitored through several methods:
	 * <p/>
	 * <UL>
	 * <LI>{@link FileTransfer#getStatus()}
	 * <LI>{@link FileTransfer#getProgress()}
	 * <LI>{@link FileTransfer#isDone()}
	 * </UL>
	 * 
	 * @param file
	 *            The location to save the file.
	 * @throws XMPPException
	 *             when the file transfer fails
	 * @throws IllegalArgumentException
	 *             This exception is thrown when the the provided file is either
	 *             null, or cannot be written to.
	 */
	public void recieveFile(final File file) throws XMPPException {
		if (file != null) {
			if (!file.exists()) {
				try {
					file.createNewFile();
				} catch (IOException e) {
					throw new XMPPException(
							"Could not create file to write too", e);
				}
			}
			if (!file.canWrite()) {
				throw new IllegalArgumentException(
						"Cannot write to provided file");
			}
		} else {
			throw new IllegalArgumentException("File cannot be null");
		}

		Thread transferThread = new Thread(new Runnable() {
			public void run() {
				try {
					inputStream = negotiateStream();
				} catch (XMPPException e) {
					handleXMPPException(e);
					return;
				}

				OutputStream outputStream = null;
				try {
					outputStream = new FileOutputStream(file);
					setStatus(Status.in_progress);
					writeToStream(inputStream, outputStream);
				} catch (XMPPException e) {
					setStatus(Status.error);
					setError(Error.stream);
					setException(e);
				} catch (FileNotFoundException e) {
					setStatus(Status.error);
					setError(Error.bad_file);
					setException(e);
				}

				if (getStatus().equals(Status.in_progress)) {
					setStatus(Status.complete);
				}
				if (inputStream != null) {
					try {
						inputStream.close();
					} catch (Throwable io) {
						/* Ignore */
					}
				}
				if (outputStream != null) {
					try {
						outputStream.close();
					} catch (Throwable io) {
						/* Ignore */
					}
				}
			}
		}, "File Transfer " + streamID);
		transferThread.start();
	}

	private void handleXMPPException(XMPPException e) {
		setStatus(FileTransfer.Status.error);
		setException(e);
	}

	private InputStream negotiateStream() throws XMPPException {
		setStatus(Status.negotiating_transfer);
		final StreamNegotiator streamNegotiator = negotiator
				.selectStreamNegotiator(recieveRequest);
		setStatus(Status.negotiating_stream);
		FutureTask<InputStream> streamNegotiatorTask = new FutureTask<InputStream>(
				new Callable<InputStream>() {

					public InputStream call() throws Exception {
						return streamNegotiator
								.createIncomingStream(recieveRequest
										.getStreamInitiation());
					}
				});
		streamNegotiatorTask.run();
		InputStream inputStream;
		try {
			inputStream = streamNegotiatorTask.get(15, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			throw new XMPPException("Interruption while executing", e);
		} catch (ExecutionException e) {
			throw new XMPPException("Error in execution", e);
		} catch (TimeoutException e) {
			throw new XMPPException("Request timed out", e);
		} finally {
			streamNegotiatorTask.cancel(true);
		}
		setStatus(Status.negotiated);
		return inputStream;
	}

	public void cancel() {
		setStatus(Status.cancelled);
	}

}
